/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.action;

import java.util.*;
import javax.swing.*;
import ch.simpleel.util.*;
import ch.trackedbean.binding.action.ActionManager.IActionHolder;
import ch.trackedbean.binding.action.MenuPath.MenuPathSegment;

/**
 * Helper for populating a {@link JPopupMenu}/{@link JMenu} .<br>
 * Simply add the desired actions using {@link #addAction(ActionEntry)} then call {@link #populate(JComponent)} followed by {@link #clear()}.<br>
 * This class is NOT thread safe!
 * 
 * @author M. Hautle
 */
public class MenuManager implements IActionHolder {
    /** The list with the added actions. */
    private final List<ActionEntry<?>> entries = new ArrayList<ActionEntry<?>>();

    /**
     * {@inheritDoc}
     */
    public void addAction(ActionEntry e) {
        int index = Collections.binarySearch(entries, e);
        if (index < 0)
            index = -index - 1;
        entries.add(index, e);
    }

    /**
     * Populates the given component with the actions previously added by {@link #addAction(ActionEntry)}.<br>
     * The current content of the component will be removed!<br>
     * You should call {@link #clear()} after this method to clean up the manager.
     * 
     * @param <T> The component type
     * @param comp The component to populate
     * @return The passed component
     */
    public <T extends JComponent> T populate(T comp) {
        return populate(comp, false);
    }

    /**
     * Populates the given component with the actions previously added by {@link #addAction(ActionEntry)}.<br>
     * You should call {@link #clear()} after this method to clean up the manager.
     * 
     * @param <T> The component type
     * @param comp The component to populate
     * @param append True if the actions should be appended after the current component content, false if the previous content of the component should be
     *            removed first
     * @return The passed component
     */
    public <T extends JComponent> T populate(T comp, boolean append) {
        if (!append)
            comp.removeAll();
        if (entries.isEmpty())
            return comp;
        final Stack stack = new Stack(comp, getFirstEntriesGroup());
        for (ActionEntry a : entries)
            addEntry(stack.getMenu(a.getPath()), a);
        return comp;
    }

    /**
     * Returns the group of the first {@link ActionEntry} in {@link #entries}.
     * 
     * @return The group (may be null)
     */
    private String getFirstEntriesGroup() {
        final MenuPath path = entries.get(0).getPath();
        final MenuPathSegment[] pathElements = path.getPath();
        if (pathElements.length > 0)
            return pathElements[0].getGroup();
        return path.getLastGroup();
    }

    /**
     * Adds the given entry to the passed menu.
     * 
     * @param menu The menu were to place the entry
     * @param entry The entry
     */
    protected void addEntry(JComponent menu, ActionEntry entry) {
        menu.add(new JMenuItem(entry.getAction()));
    }

    /**
     * Returns wherever this manager contains action entries.
     * 
     * @return True if there are action entries
     */
    public boolean hasEntries() {
        return entries.size() > 0;
    }

    /**
     * Clears the action entries and makes this manager ready for the next menu.
     */
    public void clear() {
        entries.clear();
    }

    /**
     * Stack holding the current submenu path.
     * 
     * @author M. Hautle
     */
    private static class Stack {
        /** The work stack. The entries contained in it will be recycled. */
        final List<Entry> stack = new ArrayList<Entry>(2);

        /** The root component. */
        final JComponent root;

        /**
         * Default constructor.
         * 
         * @param c The root component
         * @param group The group of the first entry
         */
        public Stack(JComponent c, String group) {
            final Entry e = new Entry();
            e.group = group;
            root = c;
            stack.add(e);
        }

        /**
         * Returns the menu for the given path.<br>
         * If this menu does not yet exists it will be created.<br>
         * This method inserts also a {@link JSeparator} for each group change.
         * 
         * @param path The path
         * @return The menu for the path
         */
        JComponent getMenu(MenuPath path) {
            final MenuPathSegment[] segments = path.getPath();
            prepare(segments.length + 1);
            final int fork = getPathFork(segments);
            // the current submenu stack represents our path
            if (fork == segments.length)
                return getPeekMenu(path);
            JComponent parentMenu = getMenu(fork);
            // add a separator if there was a group change
            if (!MiscUtils.equals(segments[fork].getGroup(), stack.get(fork).group))
                parentMenu.add(new JSeparator());
            // create the missing sub menus
            for (int i = fork, cnt = segments.length; i < cnt; i++) {
                final Entry e = stack.get(i);
                final String name = segments[i].getSubMenu();
                e.name = name;
                e.menu = new JMenu(name);
                e.group = segments[i].getGroup();
                parentMenu.add(e.menu);
                // our entry is the new parent menu
                parentMenu = e.menu;
            }
            // setup the last entry - this is only a group
            final Entry e = stack.get(segments.length);
            e.group = path.getLastGroup();
            e.name = null;
            e.menu = null;
            return getMenu(segments.length);
        }

        /**
         * Returns the menu component of the stack element before the given one.
         * 
         * @param index The index
         * @return The menu component
         */
        private JComponent getMenu(int index) {
            if (index == 0)
                return root;
            return stack.get(index - 1).menu;
        }

        /**
         * Special case for {@link #getMenu(int)} - if the {@link MenuPath#getPath()} of the given path corresponds to the one on the stack.<br>
         * This method inserts a {@link JSeparator} if the group changed.
         * 
         * @param path The path
         * @return The menu
         */
        private JComponent getPeekMenu(MenuPath path) {
            final String group = path.getLastGroup();
            final int len = path.getPath().length;
            final JComponent m = getMenu(len);
            final Entry peek = stack.get(len);
            // add a separator if there was a group change
            if (!MiscUtils.equals(group, peek.group)) {
                m.add(new JSeparator());
                peek.group = group;
            }
            return m;
        }

        /**
         * Returns the index in the path were the first difference to the stack exists.
         * 
         * @param path The path
         * @return The index of the change or <code>path.length</code> if no change occured
         */
        private int getPathFork(MenuPathSegment[] path) {
            for (int i = 0; i < path.length; i++) {
                final Entry e = stack.get(i);
                final String name = path[i].getSubMenu();
                final String group = path[i].getGroup();
                // assert same name and group
                if (!MiscUtils.equals(e.group, group) || !MiscUtils.equals(e.name, name))
                    return i;
            }
            return path.length;
        }

        /**
         * Ensures that the {@link #stack} has at least space for the given number of entries.
         * 
         * @param length The length
         */
        private void prepare(int length) {
            for (int i = stack.size(); i < length; i++)
                stack.add(new Entry());
        }
    }

    /**
     * Stack entry.
     * 
     * @author M. Hautle
     */
    private static class Entry {
        /** The menu component or null. */
        JComponent menu;

        /** The submenu name or null. */
        String name;

        /** The group name or null. */
        String group;
    }
}
